-- EquipM
-- Create by panyl
-- 装备

module("EquipM", package.seeall);

-- 所有的装备信息
local equips = {};
local specialEquipsUpgrade = {};

-- 收集每个宠物对应的时装
local petCostumeList = {};

-- 装备树
local equipTree = {};

-- 载入配置表
local _init = false;
local loadCsv = function()
    if _init then return end
    _init = true;

    -- 转换下equip信息表
    local equip_CSV = get_equip_CSV();
    for i = 1, #equip_CSV do
        local classId = equip_CSV[i].class_id;
        equips[classId] = equip_CSV[i];

        local nextClassId = equip_CSV[i].next_class_id;
        if type(nextClassId) == 'number' and nextClassId > 0 then
            equipTree[nextClassId] = classId;
        end

        -- 收集宠物时装列表
        if equip_CSV[i].type == EQUIP_TYPE_COSTUME then
            local petId = equip_CSV[i].dbase["pet_id"];
            if not petId then
                error("时装("..classId..")未配置对应英雄ID");
            end

            if not petCostumeList[petId] then
                petCostumeList[petId] = {};
            end

            table.insert(petCostumeList[petId], classId);
        end
    end

    local special_equip_upgrade_CSV = get_special_equip_upgrade_CSV();
    for i = 1, #special_equip_upgrade_CSV do
        local classId = special_equip_upgrade_CSV[i].class_id;
        specialEquipsUpgrade[classId] = special_equip_upgrade_CSV[i];
    end
    special_equip_upgrade_CSV = {};
end

-- 模块初始化
function init()
    loadCsv();
end

-- 检索装备信息
function query(classId, path)
    local m = equips[classId];
    if m == nil then
        return nil;
    end
    if (path == "" or path == nil) then
        return m;
    else
        return m[path];
    end
end

-- 获取阶位
function getRealRank(classId)
    if DesertAltarM.isAladdinLamp(classId) then
        -- 阿拉丁神灯
        return DesertAltarM.getLampRank();
    elseif HolyItemsM.isHolyItems(classId) then
        -- 圣器
        return HolyItemsM.getRank(classId);
    elseif ArtifactM.isArtifact(classId) then
        local rank = FormulaM.invoke("CALC_ARTIFACT_RANK", classId);
        return rank;
    elseif isSpecialUpEquip(classId) then
        local level = getSpecialEquipLevel(classId);
        local rank = FormulaM.invoke("CALC_SPECIAL_EQUIP_RANK", classId, level);
        return rank;
    elseif SkyInstanceM.isDragonRing(classId) then
        -- 炎龙の吐息
        return SkyInstanceM.getDragonRingRank();
    elseif FormulaM.invoke("IS_ELEMENT_BALL", classId) then
        -- 元素套合成的装备
        local rank = FormulaM.invoke("CALC_ELEMENT_BALL_RANK", classId);
        return rank;
    end

    return query(classId, "rank");
end

-----------------------------------------------------
-- 是否装备了某件装备
-- @function [parent=#EquipM] isEquip
-- @param classId 道具ID
function isEquiped(classId)
    local equips = ME.user.equipments;

    for _, t in pairs(equips) do
        if classId == t[1] then
            return true;
        end
    end

    return false;
end

-- 获取玩家所有装备
function queryUserAllEquip()
    local ret = {}
    local items = ME.user.items
    for classId, item in pairs(items) do
        if isEquipment(classId) and ItemM.query(classId, "type") ~= ITEM_TYPE_ARTIFACT and ItemM.query(classId, "type") ~= ITEM_TYPE_ARTIFACT_SCRAP then
            ret[classId] = classId;
        end
    end

    return ret
end

-- 判断用户是否拥有此装备
function hasEquipment(classId)
    if ItemM.getAmount(ME.user, classId) > 0 then
        -- 在包裹中
        return true;
    end

    -- 穿在身上
    if isEquiped(classId) then
        return true;
    end

    return false;
end

-- 获取已经装备上的装备
function getEquip(type, pos)
    pos = pos or 1;
    local equipments = ME.user.equipments;
    local equiped = equipments[type] or {};
    return equiped[pos];
end

-- 获取所有的宝物
function getAllTreasure()
    local ret = {};
    for classId, info in pairs(equips) do
        -- 不是装备、时装和徽章（还需要过滤神器）
        if info.type ~= EQUIP_TYPE_WEAPON and
           info.type ~= EQUIP_TYPE_MEDAL and
           info.type ~= EQUIP_TYPE_COSTUME and
           not ArtifactM.isArtifact(info.class_id) then
            table.insert(ret, classId);
        end
    end

    return ret;
end

-- 获取玩家所有宝物列表
function getUserTreasureList()
    -- 身上穿的装备
    local equipedList = {};
    local equipType = { EQUIP_TYPE_TRUMP, EQUIP_TYPE_RING, EQUIP_TYPE_NECKLACE,
                        EQUIP_TYPE_HELMET, EQUIP_TYPE_CLOAK, EQUIP_TYPE_ARMOR,
                        EQUIP_TYPE_GLOVE, EQUIP_TYPE_WEAPON, EQUIP_TYPE_MEDAL,
                        EQUIP_TYPE_BOOT, EQUIP_TYPE_BELT, EQUIP_TYPE_RISKER};

    for i = 1, 12 do
        local classId = EquipM.getEquip(equipType[i], 1);
        if classId ~= nil and classId ~= 0 and not ArtifactM.isArtifact(classId) then
            table.insert(equipedList, classId);
        end
    end

    -- 所有装备
    local treasures = getAllTreasure();

    local allEquips = {};
    for i = 1, #treasures do
        local itemAmount = ItemM.getAmount(ME.user, treasures[i]);
        if table.indexOf(equipedList, treasures[i]) ~= -1 then
            -- 身上有装备
            itemAmount = itemAmount + 1;
        end

        if itemAmount > 0 then
            allEquips = table.add(allEquips, { [treasures[i]] = itemAmount, });
        end
    end

    return allEquips;
end

-- 获取当前所有的某类装备
function queryEquipments(type)
    local ret = {}
    local items = ME.user.items
    for classId, item in pairs(items) do
        -- 期望的装备
        if isEquipment(classId) and query(classId, "type") == type then
            if query(classId, "type") == type then
                ret[classId] = item;
            end
        end
    end

    return ret
end

-- 判断某物品是否是装备
function isEquipment(classId)
    -- 没有该类装备
    if not equips[classId] then
        return false
    end

    return true
end

-- 能否能装备
function canEquip(classId)
    -- 没有该装备
    if not equips[classId] then
        return false
    end

    -- 如果玩家没有该装备
    if ItemM.getAmount(ME.user, classId) <= 0 then
        return false
    end

    -- 需求
    local require = query(classId, "require") or {};
    for attrib, value in pairs(require) do
        -- 该装备不能装备
        if attrib == "not_equip" then
            doAlert(getLocStr("forbid_wear_equip"));
            return false;
        end

        if ME.user:queryAttrib(attrib) < value then
            doAlert(getLocStr("forbid_wear_equip"));
            return false;
        end
    end

    -- 只要有就可以装备
    return true
end

-- 判断某件装备是否穿在身上
function isEquiped(classId)
    -- 已装备的
    local equipments = ME.user.equipments or {};
    if table.keys(equipments) == 0 then
        -- 没有穿戴任何装备
        return false;
    end

    for _, arr in pairs(equipments) do
        if table.indexOf(arr, classId) ~= -1 then
            return true;
        end
    end

    return false;
end

-- 装备
function equip(classId, forceEquip, forceChange)
    if not forceEquip and not DungeonM.checkDungeonAction() then
        -- 经检测，不允许执行地牢行为
        return false;
    end

    -- 经判断不能装备
    if not canEquip(classId) then
        return false
    end

    -- 替换装备
    if not forceEquip and forceChange then
        if ME.user:queryAttrib("hp") <= 0 then
            doAlert(getLocStr("change_equip_hp_not_enough"));
            return false;
        elseif ME.user:queryAttrib("mp") < 0 then
            doAlert(getLocStr("change_equip_mp_not_enough"));
            return false;
        end
    end

    -- 已装备的
    local equipments = ME.user.equipments
    local type = query(classId, "type")     -- 装备类型
    local equiped = equipments[type] or {}  -- 已装备的该类装备
    local maxNum = FormulaM.invoke("FETCH_CAN_EQUIP_AMOUNT_LIMIT", type) -- 可装备数量

    -- 装备满了，把最后一个卸下
    local oldEquiped = table.deepcopy(equiped);
    local changeEquip = false;
    if #equiped >= maxNum then
        if not unequip(type, maxNum, forceChange) then
            return false;
        end

        changeEquip = true;
    end

    -- 扣除物品
    ItemM.costAmount(ME.user, classId, 1);

    -- 装备上
    equiped[#equiped + 1] = classId;
    equipments[type] = equiped;
    ME.user.equipments = equipments;

    -- 刷新下属性
    PropM.refresh(ME.user)

    -- 尝试添加套装激活记录
    tryRecordSuitActive(classId);

    DungeonLogM.addLog(string.format("穿戴装备：%d", classId));

    -- 判断下血量
    if not forceEquip and changeEquip then
        if ME.user:queryAttrib("hp") <= 0 then
            doAlert(getLocStr("equip_hp_not_enough"));
            equip(oldEquiped[maxNum], true, true);

            EventMgr.fire(event.EQUIP, oldEquiped[maxNum])
            return false;
        elseif ME.user:queryAttrib("mp") < 0 then
            doAlert(getLocStr("equip_mp_not_enough"));
            equip(oldEquiped[maxNum], true, true);

            EventMgr.fire(event.EQUIP, oldEquiped[maxNum])
            return false;
        end
    end

    -- 穿上装备事件
    if not forceEquip then
        -- 穿戴属性
        local props = calcBaseProp(ME.user, classId);
        -- 获取工程学手册对装备的额外加成
        local manualAddition = EngineeringManualM.getManualAddition(classId, "equip");
        if manualAddition then
            props = table.append(props, manualAddition);
        end

        for _, prop in pairs(props) do
            if PropM.query(prop[1], "trigger") == "wear" then
                -- 触发一次
                PropM.trigger(ME.user, prop[1], prop[2], classId);
            end
        end

        EventMgr.fire(event.EQUIP, classId);
    end

    -- 锻造师专属发动特效
    local prop = PropM.combine(ME.user, "equip_to_attrib", "attack");
    if prop[3] > 0 then
        EventMgr.fire(event.SPECIAL_SKILL_TRIGGER, { propId = prop[1] });
    end

    return true
end

-- 尝试添加套装激活记录
function tryRecordSuitActive(classId)
    local suitId = EquipM.query(classId, "suit");
    if type(suitId) ~= 'number' or suitId <= 0 then
        -- 不是套装，无视
        return false;
    end

    local activeList = SuitM.query(suitId, "active");
    if #activeList  == 0 then
        return false;
    end

    -- 判断是否凑齐了套装
    local isActive = true;
    for _, equipId in pairs(activeList) do
        if not EquipM.isEquiped(equipId) then
            isActive = false;
        end
    end

    if not isActive then
        -- 没有凑齐套装
        return false;
    end

    local activeSuits = ME.user.dbase:query("active_suits", {});
    if table.indexOf(activeSuits, suitId) == -1 then
        -- 标记该套装激活过
        table.insert(activeSuits, suitId);
        ME.user.dbase:set("active_suits", activeSuits);
    end

    -- 抛出事件
    EventMgr.fire(event.SUIT_ACTIVE,  {["suitId"] = suitId, });

    return true;
end

-- 能否卸下
function canUnequip(type, pos, forceChange)
    -- 血量少于0的情况不能脱
    if not forceChange and ME.user:queryAttrib("hp") <= 0 then
        return false;
    end

    local equipments = ME.user.equipments
    local equiped = equipments[type]

    -- 没有装备
    if not equiped or #equiped <= 0 then
        return false
    end

    local pos = pos or 1
    if not equiped[pos] then
        return false
    end

    -- TODO
    -- 考虑到更换装备的情况
--    local props = EquipM.query(equiped[pos], "prop");
--    for i = 1, #props do
--        if props[i][2] == "max_hp" then
--            if props[i][3] >= ME.user:getHp() then
--                return false;
--            end
--        elseif props[i][2] == "max_mp" then
--            if props[i][3] > ME.user:getMp() then
--                return false;
--            end
--        end
--    end

    return true
end

-- 卸下
function unequip(type, pos, forceChange)
    if not forceChange and not DungeonM.checkDungeonAction() then
        -- 经检测，不允许执行地牢行为
        return false;
    end

    -- 经判断不能卸下
    if not canUnequip(type, pos, forceChange) then
        return false;
    end

    local pos = pos or 1;
    local equipments = ME.user.equipments;
    local equiped = equipments[type];

    -- 该装备的classId
    local classId = equiped[pos];

    -- 删除该装备
    table.remove(equiped, pos);
    if #equiped <= 0 then
        equipments[type] = nil;
    end

    -- 放到玩家身上
    ItemM.addAmount(ME.user, classId, 1, { ["existed"] = true, });

    -- 刷新下属性
    PropM.refresh(ME.user);

    -- 如果脱下来之后血量少于0，还需要穿上去
    if not forceChange then
        -- 没有强制替换
        if ME.user:queryAttrib("hp") <= 0 then
            doAlert(getLocStr("equip_hp_not_enough"));
            equip(classId, true);
            return false;
        elseif ME.user:queryAttrib("mp") < 0 then
            doAlert(getLocStr("equip_mp_not_enough"));
            equip(classId, true);
            return false;
        end
    end

    -- 卸下装备事件
    EventMgr.fire(event.UNEQUIP, { ["type"] = type, ["pos"] = pos, ["classId"] = classId, });
    return true;
end

-- 获取玩家所有的装备属性加成
function fetchAllProps(who)
    local equipments = who.equipments
    local props = {};
    local prop = {};

    -- 如果是宠物，先获取旧装备的属性
    if who.type == OBJECT_TYPE_PET then
        props = table.append(props, PetM.fetchOldEquipProps(who));
    end

    -- 如果是召唤兽，需要到召唤兽模块处理
    if who.type == OBJECT_TYPE_SUMMON then
        return SummonM.getEquipmentProps(who);
    end

    -- 没有装备
    if not equipments or #table.keys(equipments) <= 0 then
        return props;
    end

    local equipList = {};
    -- 遍历每个部位
    for _, equiped in pairs(equipments) do
        -- 遍历该部位的所有已穿戴装备
        for _, e in pairs(equiped) do
            if isEquipment(e) then
                -- 如果是装备则获取该装备的属性，这里需要深度拷贝一个新的table
                local temp = calcBaseProp(who, e);
                prop = table.deepcopy(temp);
                table.insert(equipList, e);

                -- 获取工程学手册对装备的额外加成
                local manualAddition = EngineeringManualM.getManualAddition(e, "equip");
                if manualAddition then
                    prop = table.append(prop, manualAddition);
                end

                -- 如果是神器，加上神器升级带来的附加属性
                if ArtifactM.isArtifact(e) then
                    prop = table.append(prop, ArtifactM.getExtraProps(e));
                end

                -- 如果是拉丁神灯，需要获取额外属性
                if DesertAltarM.isAladdinLamp(e) then
                    prop = table.append(prop, DesertAltarM.getLampProp(e));
                end

                -- 如果是圣器，加上圣器的附加属性
                if HolyItemsM.isHolyItems(e) then
                    prop = table.append(prop, HolyItemsM.getExtraProps(e));
                end

                -- 此处如果有额外属性加成请在装备替换界面
                -- UIEquipsOperation:getAttrib里也加上相关处理
                -- SLIMEC-2536

                -- 如果是专属装备
                if SpecialEquipM.isSpecialEquip(e) then
                    prop = table.append(prop, SpecialEquipM.fetchEquipProps(e));
                end

                -- 如果是神器且有神器效果加成
                if ArtifactM.isArtifact(e) and #PropM.fetchProps(who, "add_artifact") > 0 then
                    local arr = PropM.combine(who, "add_artifact", 1);
                    for index = 1, #prop do
                        -- 称号强化项、减少升级消耗，除外
                        if prop[index][1] ~= PropM.getPropId("career_strengthen") and
                            prop[index][1] ~= PropM.getPropId("skill_cost") then
                            prop[index][3] = PropM.apply(arr, prop[index][3])
                        end
                    end

                    -- 神器研究，不能有UI的东西，暂时注释掉 by panyl
                    -- if UIDungeonMgr:getCurLevel() == nil then
                    --     -- 触发特技发动事件
                    --     ME.user.dbase:setTemp("speicial_skill_trigger", arr[1]);
                    -- end
                end

                props = table.append(props, prop);
            end
        end
    end

    --判断是否激活套装属性
    props = table.append(props, SuitM.getSuitProp(equipList));

    return props;
end

-- 计算属性
function calcBaseProp(who, classId)
    local prop = query(classId, "prop");
    if type(prop) == 'table' then
        return prop;
    end

    if type(prop) == "string" then
        return FormulaM.invoke(prop, who, classId);
    end

    return {};
end

-- 获取单件装备属性
function fetchEquipProp(classId)
    if not isEquipment(classId) then
        return {};
    end

    local prop = calcBaseProp(ME.user, classId);

    -- 如果是拉丁神灯，需要获取额外属性
    if DesertAltarM.isAladdinLamp(classId) then
        prop = table.append(prop, DesertAltarM.getLampProp(classId));
    end

    return prop;
end

-- 拆解装备
function splitEquip(classId, splitCount)
    if splitCount == nil then
        splitCount = 1;
    end

    if splitCount > 65535 then
        splitCount = 65535;
    end

    -- 检查是否拥有装备
    local amount = ItemM.getAmount(ME.user, classId);
    if amount < splitCount then
        return false;
    end

    -- 扣除装备
    if not ItemM.costAmount(ME.user, classId, splitCount) then
        cclog("cost amount fail");
        return false;
    end

    -- 获取拆分后能得到的探索点
    local gainSp = FormulaM.invoke("CALC_SPLIT_EQUIP_GAIN_SP", classId) * splitCount;

    -- 增加探索点
    --ME.user:addAttrib("search_point", gainSp);
    BonusM.doBonus({ 2, "search_point", gainSp, }, "split_equip");

    -- 分解装备概率获得淬火精华
    local prop = PropM.combine(ME.user, "get_cream", 1);
    if prop[3] > 0 then
        local args = { ["classId"] = classId, ["count"] = splitCount, };
        PropM.trigger(ME.user, prop[1], prop[2], args);
    end

    -- 拆分装备事件
    EventMgr.fire(event.SPLIT_EQUIP, { classId = classId, count = splitCount, gainSp = gainSp });
    return true;
end

-- 转化装备（已穿戴的）
function transformEquipment(src, dst)
    -- 需要遍历已经穿戴的装备，找到src，然后直接变成dst;若dst为0，直接干掉装备
    local equipments = ME.user.equipments;

    for type, list in pairs(equipments) do
        if #list > 0 and list[1] == src then
            if dst == 0 then
                table.remove(list, 1);
                if #list <= 0 then
                    equipments[type] = nil;
                end
            else
                list[1] = dst;
            end
            break;
        end
    end

    -- 刷新下属性
    PropM.refreshEffect(ME.user, "equip");
end

------------------------------------
-- 英雄能否穿戴装备
-- @param pet      英雄对象
-- @param classId  装备编号
function canHeroEquip(pet, classId)
    -- 装备不存在
    if not equips[classId] then
        return false;
    end

    -- 玩家没有该装备
    if ItemM.getAmount(ME.user, classId) <= 0 then
        return false;
    end

    -- 调用公式判断
    if not FormulaM.invoke("CAN_HERO_EQUIP", pet, classId) then
        return false;
    end

    return true;
end

------------------------------------
-- 英雄穿戴装备
-- @param pet      英雄对象
-- @param classId  装备编号
-- @return 穿戴是否成功
function heroEquip(pet, classId)
    -- 经判断，不能穿戴
    if not canHeroEquip(pet, classId) then
        return false;
    end

    -- 已装备的
    local equipments = pet.equipments
    local type = query(classId, "type")     -- 装备类型
    local equiped = equipments[type] or {}  -- 已装备的该类装备
    local maxNum = FormulaM.invoke("FETCH_CAN_EQUIP_AMOUNT_LIMIT", type) -- 可装备数量

    -- 装备满了，把最后一个卸下
    if #equiped >= maxNum then
        if not heroUnEquip(pet, type, maxNum) then
            return false;
        end
    end

    -- 扣除物品
    ItemM.costAmount(ME.user, classId, 1);

    -- 装备上
    equiped[#equiped + 1] = classId;
    equipments[type] = equiped;
    pet:setEquipments(equipments);

    -- 刷新下属性
    PropM.refresh(pet);

    -- 如果是时装，需要设置模型ID
    if type == EQUIP_TYPE_COSTUME then
        local modelId = EquipM.queryPetModelID(classId);
        pet:setModelId(modelId);
    end

    -- 抛出英雄穿上装备事件
    EventMgr.fire(event.EQUIP, classId);

    -- 如果是徽章/武器，需要单独抛个事件出来
    if type == EQUIP_TYPE_WEAPON or type == EQUIP_TYPE_MEDAL then
        EventMgr.fire(event.EQUIP_MEDAL_OR_WEAPON, { ["classId"] = classId, ["petId"] = pet:query("class_id"), });
    end

    return true;
end

------------------------------------
-- 英雄能否卸下装备
-- @param pet      英雄对象
-- @param type     装备类型
-- @param pos      装备位置
function canHeroUnequip(pet, type, pos)
    local equipments = pet.equipments;
    local equiped = equipments[type];

    -- 没有装备
    if not equiped or #equiped <= 0 then
        return false;
    end

    local pos = pos or 1;
    if not equiped[pos] then
        return false;
    end

    return true;
end

-------------------------------------
-- 英雄卸下装备
-- @param pet      英雄对象
-- @param type     装备类型
-- @param pos      装备位置
function heroUnEquip(pet, type, pos)
    -- 经判断，不能卸下
    if not canHeroUnequip(pet, type, pos) then
        return false;
    end

    local pos = pos or 1;
    local equipments = pet.equipments;
    local equiped = equipments[type];

    -- 该装备的classId
    local classId = equiped[pos];

    -- 删除该装备
    table.remove(equiped, pos)
    if #equiped <= 0 then
        equipments[type] = nil;
    end

    -- 放到玩家身上
    ItemM.addAmount(ME.user, classId, 1, { ["existed"] = true, });

    -- 刷新下属性
    PropM.refresh(pet)

    -- 如果是时装，需要重置模型ID
    if type == EQUIP_TYPE_COSTUME then
        local defaultModelId = PetM.query(pet.classId, "default_model");
        pet:setModelId(defaultModelId);
    end

    -- 卸下装备事件
    EventMgr.fire(event.UNEQUIP, { ["type"] = type, ["pos"] = pos, ["classId"] = classId, });

    return true;
end

------------------------------------------
-- 获取英雄已经装备上的装备
-- @param pet      英雄对象
-- @param type     装备类型
-- @param pos      装备的位置
function getHeroEquip(pet, type, pos)
    local equipments = pet.equipments;
    local equiped = equipments[type] or {};
    local classId = equiped[pos];

    return classId;
end

------------------------------------------
-- 获取装备属性描述
-- @param props      原始属性
function getEquipPropDesc(props, classId, isEquiped)
    -- 拼合属性描述的时候
    -- 如果是属性+ (propId为200)，用"、"隔断，不换行(最多连续3个)
    -- 其他情况要换行
    local propDesc = "";
    local specialDesc = {};
    local total = #props;
    -- 只需要显示detail的特殊装备
    local detailEquips = { 7918 };

    for index, var in pairs(props) do
        local desc = PropM.getPropDesc(var);

        if var[1] == PropM.getPropId("attrib2") and
           table.indexOf({"fire","armor","speed","luck","fight_ability"}, var[2]) < 0 then
            -- 先缓存起来
            table.insert(specialDesc, desc);
        else
            local preDesc = "";
            -- 其他属性，需要把前面的attrib2属性拼接起来
            if #specialDesc >= 4 then
                -- 前面2条作为一行
                preDesc = string.implode(table.slice(specialDesc, 1, 2), getLocStr("seperator")) .. "\n";
                preDesc = preDesc .. string.implode(table.slice(specialDesc, 3), getLocStr("seperator")) .. "\n";
            elseif #specialDesc > 0 then
                -- 少于4条属性，作为一行
                preDesc = string.implode(specialDesc, getLocStr("seperator")) .. "\n";
            end

            -- 清空
            specialDesc = {};

            desc = preDesc .. desc;

            if index ~= total then
                -- 不是最末尾的
                desc = desc .. "\n";
            end

            propDesc = propDesc .. desc;
        end
    end

    -- 其他属性，需要把前面的attrib2属性拼接起来
    if #specialDesc >= 4 then
        -- 前面2条作为一行
        propDesc = propDesc .. string.implode(table.slice(specialDesc, 1, 2), getLocStr("seperator")) .. "\n";
        propDesc = propDesc .. string.implode(table.slice(specialDesc, 3), getLocStr("seperator"));
    elseif #specialDesc > 0 then
        -- 少于4条属性，作为一行
        propDesc = propDesc .. string.implode(specialDesc, getLocStr("seperator"));
    end

    propDesc = propDesc.sub(propDesc, 1, propDesc.len(propDesc));

    -- 特殊属性描述
    local specialDesc = query(classId, "special_prop_desc");
    if type(specialDesc) == "string" and string.len(specialDesc) > 0 then
        if string.startWith(specialDesc, "formula_") then
             local prefix = "formula_";
             local formulaName = string.sub(specialDesc, string.len(prefix) + 1);
             propDesc = FormulaM.invoke(formulaName, propDesc, props, classId, isEquiped);
        else
            propDesc = propDesc .. specialDesc;
        end
    end

    -- 如果是自销毁装备，需要增加进度信息
    if classId then
        propDesc = propDesc .. SelfDestructM.SelfDestructDesc(classId, isEquiped);

        local detail = query(classId, "detail");

        -- 特殊装备只显示detail即可
        if table.indexOf(detailEquips, classId) ~= -1 then
            return detail;
        end

        if type(detail) == "string" and #detail > 0 then
            propDesc = propDesc .. "\n" .. detail;
        end
    end

    -- 专属装备
    if SpecialEquipM.isSpecialEquip(classId) then
        propDesc = propDesc .. SpecialEquipM.getSpecialDesc(classId);
    end

    -- 抽取掉颜色等信息
    propDesc = extractString(propDesc);

    -- 工程学手册的加成需要显示为绿色
    local extraDesc = EngineeringManualM.getEquipAdditionDesc(classId);
    if extraDesc and extraDesc ~= "" then
        if propDesc == "" then
            propDesc = propDesc .. "[green]" .. extraDesc .. "[-]";
        else
            propDesc = propDesc .. "\n[green]" .. extraDesc .. "[-]";
        end
    end

    return propDesc;
end

-- 获取初级装备
function findBaseEquip(classId)
    local level = EquipM.query(classId, "level");
    if not level or level == 1 then
        -- 没有配置等级，或者等级为1，默认就是初级装备
        return classId;
    end

    while equipTree[classId] do
        classId = equipTree[classId];
    end

    return classId;
end

-- 获取最高等级的装备
function findMaxEquip(classId)
    local baseEquip = findBaseEquip(classId);
    local next = query(baseEquip, "next_class_id");

    while type(next) == 'number' and next > 0 do
        classId = next;
        next = query(classId, "next_class_id");
    end

    return classId;
end

-- 穿戴装备触发
function doWearTrigger(who, classId, propId, attrib)
    local wearEquip = who.dbase:query("wear_equip", {});
    local  key = string.format("%d_%d_%s", classId, propId, tostring(attrib));

    if wearEquip[key] then
        return false;
    end

    wearEquip[key] = 1;

    who.dbase:set("wear_equip", wearEquip);

    return true;
end

-- 升级装备
function upgradeEquip(classId, nextClassId)
    if not isEquipment(classId) then
        trace("EquipM", "升级装备%d失败：不是装备。", classId);
        return false;
    end

    nextClassId = nextClassId or query(classId, "next_class_id");
    if type(nextClassId) ~= "number" then
        trace("EquipM", "升级装备%d失败：下一级装备编号不存在。", classId);
        return false;
    end

    if not isEquipment(classId) then
        trace("EquipM", "升级装备%d失败：下一级装备%d不存在。", classId, nextClassId);
        return false;
    end

    if not hasEquipment(classId) then
        trace("EquipM", "升级装备%d失败：玩家没有该装备。", classId);
        return false;
    end

    -- 将装备替换成下一级的
    if ItemM.getAmount(ME.user, classId) > 0 then
        ItemM.costAmount(ME.user, classId, 1);
        BonusM.doBonus({ 1, nextClassId, 1 }, "upgrade_equip");
    else
        local type = EquipM.query(classId, "type");
        local equipments = ME.user.equipments;
        local equipped   = equipments[type];
        local pos   = 1;
        equipped[1] = nextClassId;
    end

    trace("EquipM", "装备%d成功升级为%d", classId, nextClassId);

    -- 刷新下属性
    PropM.refreshEffect(ME.user, "equip");

    -- 抛出装备升级事件
    EventMgr.fire(event.UPGRADE_EQUIP, { ["classId"] = classId, ["nextClassId"] = nextClassId, });

    return true;
end

-- 检索特殊升级装备信息
function querySpecial(classId, path)
    local m = specialEquipsUpgrade[classId];
    if m == nil then
        return nil;
    end
    if (path == "" or path == nil) then
        return m;
    else
        return m[path];
    end
end

-- 是否是特殊升级的装备
function isSpecialUpEquip(classId)
    local levelField = querySpecial(classId, "level_field");

    if type(levelField) ~= "string" then
        --trace("EquipM", "不是特殊升级装备:%d。", classId);
        return false;
    end
    return true;
end

-- 特殊装备的level
function getSpecialEquipLevel(classId)
    if not isSpecialUpEquip(classId) then
        --trace("EquipM", "不是特殊升级装备:%d。", classId);
        return 0;
    end

    local levelField = querySpecial(classId, "level_field");

    local curLevel = tonumber(DungeonServiceM.queryDungeonAttrib(levelField));
    -- 初始都是0，需要加1
    return curLevel + 1;
end

-- 能否升级装备
function canUpgradeSpecialEquip(classId)
    local levelField = querySpecial(classId, "level_field");
    local maxLevel = querySpecial(classId, "max_level");

    if ItemM.getAmount(ME.user, classId) <= 0 and not isEquiped(classId) then
        trace("EquipM", "不能升级装备%d：玩家没有该物品。", classId);
        return false;
    end

    if not levelField then
        trace("EquipM", "不能升级装备%d：物品dbase中没有level_field字段。", classId);
        return false;
    end

    local curLevel = getSpecialEquipLevel(classId);
    if type(maxLevel) == "number" and maxLevel <= curLevel then
        trace("EquipM", "不能升级装备%d：物品已经满级。", classId);
        return false;
    end

    return true;
end

-- 升级装备（只升级等级，不替换物品）
function upgradeSpecialEquip(classId)
    if not canUpgradeSpecialEquip(classId) then
        return false;
    end

    local levelField = querySpecial(classId, "level_field");
    DungeonServiceM.addDungeonAttrib(levelField, 1);

    local curLevel = tonumber(DungeonServiceM.queryDungeonAttrib(levelField));
    trace("EquipM", "装备%d成功升级为%d级", classId, curLevel);

    -- 刷新下属性
    PropM.refreshEffect(ME.user, "equip");

    EventMgr.fire(event.UPGRADE_SPECIAL_EQUIP, {["classId"] = classId,});

    return true;
end

-- 增加装备经验
function addSpecialEquipExp(classId, exp)
    local expField = querySpecial(classId, "exp_field");

    if type(expField) ~= "string" then
        trace("EquipM", "没有特殊升级装备经验字段:%d。", classId);
        return false;
    end

    -- 增加经验
    DungeonServiceM.addDungeonAttrib(expField, exp);

    -- 判断能否升级
    local curExp = tonumber(DungeonServiceM.queryDungeonAttrib(expField));
    local curLevel = getSpecialEquipLevel(classId);
    local needExp = calcUpgradeEquipExpNeed(classId);

    if curExp >= needExp then
        -- 扣除经验
        DungeonServiceM.costDungeonAttrib({[expField] = needExp});
        -- 升级
        return upgradeSpecialEquip(classId);
    end

    return true;
end

-- 计算升级经验
function calcUpgradeEquipExpNeed(classId)
    local upgradeExp = querySpecial(classId, "upgrade_exp");
    if type(upgradeExp) == 'number' then
        return upgradeExp;
    end

    if type(upgradeExp) == "string" then
        return FormulaM.invoke(upgradeExp);
    end

    return 0;
end

-- 扣除装备
function costEquip(classId)
    if ItemM.getAmount(ME.user, classId) > 0 then
        -- 装备在包裹中，直接扣除
        ItemM.costAmount(ME.user, classId, 1);
    elseif isEquiped(classId) then
        -- 装备穿在身上
        local type = EquipM.query(classId, "type");
        local equipments = ME.user.equipments;
        local equiped = equipments[type];

        local pos = 1;

        -- 删除该装备
        table.remove(equiped, pos);

        if #equiped <= 0 then
            equipments[type] = nil;
        end

        -- 刷新下属性
        PropM.refreshEffect(ME.user, "equip");
    else
        trace("EquipM", "没有装备(%d)", classId);
        return false;
    end

    return true;
end

-- 计算包裹中的装备产生的战斗结算奖励（穿在身上的不算）
function calcCombatStatBonus()
    local arr = getAllTreasure();
    local ret = {};
    for _, classId in pairs(arr) do
        local amount = ItemM.getAmount(ME.user, classId);
        if amount > 0 then
            local dbase = query(classId, "dbase");
            local bonus = dbase["combat_stat_bonus"];
            if type(bonus) == "table" then
                if type(bonus[1]) == "number" then
                    -- 只有一种奖励
                    table.insert(ret, { bonus[1], bonus[2], amount * bonus[3] });
                else
                    -- 有多种奖励
                    for _, arr2 in ipairs(bonus) do
                        table.insert(ret, { arr2[1], arr2[2], amount * arr2[3] });
                    end
                end
            end
        end
    end
    return ret;
end

-- 根据时装ID获取宠物模型ID
function queryPetModelID(classId)
    return query(classId, "dbase")["pet_model_id"];
end

-- 判断是否钻石购买的时装
function isGemCostume(classId)
    if classId == 0 then
        return false;
    end

    return query(classId, "dbase")["gem_costume"];
end

-- 判断是否活动兑换的时装
function isActivityCostume(classId)
    if classId == 0 then
        return false;
    end

    if query(classId, "dbase")["activity_costume"] ~= 1 then
        return false;
    end

    -- 判断是否在活动中
    return ActivityDungeonM.getCurrentActivityId() == query(classId, "dbase")["activity_id"];
end

-- 获取宠物时装列表
function getPetCostumeList(petId)
    if ServerStatusM.query(MODULE_SWITCH_OPEN_COSTUME) ~= 1 then
        -- 屏蔽时装功能，返回空列表
        return {};
    end

    return petCostumeList[petId] or {};
end

-- 是否拥有时装
function hasCostume(pet, id)
    return (ItemM.getAmount(ME.user, id) > 0) or (EquipM.getCostumeEquiped(pet) == id);
end

-- 英雄能否穿着某个装备
function getCostumeEquiped(pet)
    if not pet then
        return 0;
    end

    local equipments = pet.equipments;
    local equiped = equipments[EQUIP_TYPE_COSTUME];

    -- 没有装备
    if not equiped or #equiped <= 0 then
        return 0;
    end

    if not equiped[1] then
        return 0;
    end

    return equiped[1];
end

